feat(devnet): composable local devnet with scenarios, Docker image, and CI#7
Conversation
- Automates the build, smoke test, and publication of the devnet Docker image - Triggers on relevant code changes, pull requests, and pushes to main branch - Includes a smoke test to verify devnet health before publishing - Publishes tagged images to GitHub Container Registry (GHCR)
There was a problem hiding this comment.
Pull request overview
Adds a Bun/Foundry-powered local EFP devnet with composable scenarios, Docker support, and CI image smoke testing.
Changes:
- Introduces
setupDevnet(), devnet clients/accounts/deployment helpers, scenario utilities, and example Bun integration tests. - Adds
runDevnet.tsCLI with health endpoint plus standalone and ENS-attach Docker Compose configurations. - Adds a devnet Dockerfile and GitHub Actions workflow for building/smoke-testing/publishing the image.
Reviewed changes
Copilot reviewed 18 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
scripts/runDevnet.ts |
CLI entrypoint for starting, seeding, printing, and health-serving the devnet. |
scripts/devnet/* |
New devnet setup, deployment, encoding, scenarios, clients, accounts, shutdown, and tests. |
Dockerfile.devnet |
Builds the devnet runtime image. |
compose.yml |
Replaces old two-service local setup with single devnet service. |
compose.attach.yml |
Adds ENS-devnet attach mode. |
.github/workflows/build-devnet.yml |
Adds image build, smoke test, and publish workflow. |
package.json |
Adds devnet scripts/dependency and updates package metadata. |
.editorconfig, .dockerignore, deployments/.gitkeep |
Updates formatting/docker context support and deployment output directory. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Bun 1.x transitioned from a binary `bun.lockb` to a human-readable `bun.lock` file. This updates the project to use the new text-based lockfile format and adjusts the CI workflow accordingly for better diffing and consistency.
* Aligns both `Dockerfile` and `Dockerfile.devnet` to use the new `bun.lock` file for dependency installation. * Reflects Bun's transition from the binary `bun.lockb` to the human-readable `bun.lock` format. * Includes minor formatting adjustments for `RUN` commands.
* Adds an exception to `.dockerignore` to prevent `bun.lock` from being excluded from the Docker build context. * This is necessary as `bun.lock` has replaced `bun.lockb` as the standard lockfile for Bun 1.x. * Ensures Docker builds can properly utilize the new lockfile format for consistent dependency management, aligning with recent Dockerfile updates.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Only run the devnet build workflow on pull requests when changes are detected in source code, scripts, or libraries. This optimizes CI resource usage by preventing unnecessary builds for changes in unrelated files.
There was a problem hiding this comment.
Great work putting this devnet together! The setupDevnet utility and composable scenarios are very clean, and having a Dockerized version makes it extremely easy for frontend or indexer work.
Here is the feedback man. The most critical one is regarding bytecode reproducibility (which Copilot also flagged but wasn't addressed, or maybe it was, idk).
1. Bytecode Reproducibility (SPDX changes)
The PR description mentions that because deploy.s.sol and other deploy scripts are untouched, "the live production deployments remain reproducible".
However, changing the SPDX-License-Identifier from UNLICENSED to MIT in all .sol files actually modifies the compiled bytecode. Because foundry.toml does not set bytecode_hash = "none", the Solidity compiler appends the IPFS metadata hash (which is derived from the exact source file contents, including comments) to the bytecode.
If you need strict, byte-for-byte reproducibility with the existing production deployments (e.g. for Etherscan verification using the exact repo commit), you will either need to revert the SPDX comment changes or acknowledge that the new bytecode artifacts will no longer match the live deployments.
2. Catch unhandledRejection for graceful shutdown
In scripts/devnet/shutdown.ts, you gracefully handle uncaughtException. It would be a good idea to also handle unhandledRejection. For instance, if waitForNode() in setup.ts times out, it throws an async error that will skip your shutdown hooks and leave the process hanging or crashing ungracefully.
process.once('unhandledRejection', async (reason) => {
console.error(reason)
await shutdown('unhandledRejection', 1)
})3. CI integration for devnet.test.ts
The PR introduces a great integration test in scripts/devnet/devnet.test.ts, but it looks like .github/workflows/test.yml only runs forge test. You should consider adding a step to install bun and run bun run devnet:test in your test.yml to prevent regressions in the devnet tooling!
4. Minor parseArgs boolean behavior
Just a small heads up on parseArgs for save-deployments in runDevnet.ts. Node's parseArgs doesn't natively parse --save-deployments=false as a boolean false (it typically requires passing --no-save-deployments with specific config, or relying purely on the env var). The current setup where DEVNET_SAVE_DEPLOYMENTS=false overrides it is perfectly fine, just something to keep in mind if someone tries to disable it via CLI arguments.
* add a dedicated CI job to automate devnet integration tests * enhance devnet stability by handling unhandled promise rejections during shutdown * introduce `--no-save-deployments` flag for `runDevnet.ts`, offering clearer control over deployment persistence
Greptile SummaryIntroduces a self-contained local devnet for EFP contracts, including composable scenarios, a CLI runner, Docker support, and CI integration — without touching any existing deploy scripts or production artifacts.
Confidence Score: 5/5Safe to merge — this is a purely additive devnet tooling layer with no changes to production deploy scripts or contract source. All issues raised in earlier review rounds have been addressed: the push trigger targets master, the anvil double-stop is gone, Foundry is pinned via ARG FOUNDRY_VERSION, the private key travels only through the environment variable, and a try/finally in setupDevnet stops the anvil process on deploy failure. The encoding helpers are verified against the Solidity decodeL1ListStorageLocation layout. The remaining observations are documentation gaps rather than defects. No files require special attention; scenarios.ts has a minor documentation gap around mintList when to and manager differ, but all current callers pass them as the same account. Important Files Changed
Sequence DiagramsequenceDiagram
participant CLI as runDevnet.ts
participant Setup as setupDevnet()
participant Anvil as prool/anvil
participant Forge as forge script
participant Broadcast as broadcast artifact
participant Scenario as scenario fn
participant Health as HTTP /health
CLI->>Setup: setupDevnet(options)
alt standalone mode
Setup->>Anvil: "startAnvil({ port, chainId, ... })"
Anvil-->>Setup: "AnvilHandle { rpcUrl, stop }"
else attach mode
Setup->>Setup: waitForNode(externalRpcUrl)
end
Setup->>Forge: bun spawn forge script deploy.s.sol --broadcast
Forge-->>Broadcast: broadcast/deploy.s.sol/chainId/run-latest.json
Setup->>Broadcast: readDeploymentsFromBroadcast(chainId)
Broadcast-->>Setup: Deployments (addresses)
Setup->>Setup: getContracts(client, deployments)
Setup-->>CLI: DevnetEnvironment
CLI->>Scenario: scenarios[name](env)
Scenario->>Scenario: openPublicMint / mintList / follow / tag
CLI->>Health: httpServer.listen(healthPort)
CLI->>CLI: keepAlive() [never resolves]
CLI-->>Health: GET /health → 200 healthy
Reviews (3): Last reviewed commit: "fix(devnet): stop anvil on setup failure" | Re-trigger Greptile |
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
ensure the Anvil instance is stopped if an error occurs during the devnet setup process, preventing orphaned processes.
Summary
Adds a clean, modern local devnet for the EFP contracts — inspired by ENS's
runDevnet.tsbut lighter and built around composable scenarios that are usable directly from tests. It can also deploy EFP onto an already-running node (e.g. the ENS devnet) so both stacks share one chain.Importantly, no existing deploy files are modified —
deploy.s.sol,mint.s.sol,Deployer.sol, andfoundry.tomlare untouched, so the live production deployments remain reproducible. Deployed addresses are read from Forge's standardbroadcast/artifact instead.What's included
setupDevnet()(scripts/devnet/setup.ts) — boots a devnet and returns an environment: viem clients, named accounts, bound contract instances, andsnapshot/revert/minehelpers. Spawns anvil viaprool, or attaches to an existing node when given anrpcUrl.scripts/devnet/scenarios.ts) — small ops (openPublicMint,mintList,follow,tag) plus presets (empty,minimal,demoGraph) selectable via--scenario.scripts/runDevnet.ts) — deploy, seed a scenario, print accounts + addresses, and serve a/healthendpoint.Dockerfile.devnet,compose.yml(standalone) andcompose.attach.yml(EFP deployed onto the ENS devnet on one chain)..github/workflows/build-devnet.ymlbuilds the image, smoke-tests it via/health, and publishes toghcr.io/ethereumfollowprotocol/contracts/devnet.scripts/devnet/devnet.test.ts) demonstrating the setup → scenario → snapshot/revert pattern.Usage